package cn.cheniy.swiftmark.auth.manager;

import cn.cheniy.swiftmark.api.auth.bo.UserInfoInTokenBO;
import cn.cheniy.swiftmark.api.auth.constant.SysTypeEnum;
import cn.cheniy.swiftmark.api.auth.vo.TokenInfoVO;
import cn.cheniy.swiftmark.common.cache.constant.CacheNames;
import cn.cheniy.swiftmark.common.exception.SwiftMarkException;
import cn.cheniy.swiftmark.common.response.ResponseEnum;
import cn.cheniy.swiftmark.common.response.ServerResponseEntity;
import cn.cheniy.swiftmark.common.security.bo.TokenInfoBO;
import cn.cheniy.swiftmark.common.util.PrincipalUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author cheniy
 * @description token管理 1. 登陆返回token 2. 刷新token 3. 清除用户过去token 4. 校验token
 *
 */
@Component
@RefreshScope
public class TokenStore {

    private static final Logger logger = LoggerFactory.getLogger(TokenStore.class);

    private final RedisTemplate<Object, Object> redisTemplate;

    private final RedisSerializer<Object> redisSerializer;

    private final StringRedisTemplate stringRedisTemplate;

    public TokenStore(RedisTemplate<Object, Object> redisTemplate, RedisSerializer<Object> redisSerializer,
                      StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.redisSerializer = redisSerializer;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将用户的部分信息存储在token中，并返回token信息
     *
     * 随机生成两个Token：accessToken，refreshToken
     * 1. 将accessToken作为redis的key，用户信息作为value存放redis
     * 2. 将refreshToken作为redis的key，用户信息作为accessToken存放redis
     * 3. 将用户id作为key，用户所有的accessToken：refreshToken作为value存放redis中
     * 返回TokenInfoBO
     * @param userInfoInToken 用户在token中的信息
     * @return TokenInfoBO token信息【accessToken密文，refreshToken密文】+用户信息
     */
    public TokenInfoBO storeAccessToken(UserInfoInTokenBO userInfoInToken) {
        TokenInfoBO tokenInfoBO = new TokenInfoBO();
        String accessToken = IdUtil.simpleUUID();  // a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
        String refreshToken = IdUtil.simpleUUID(); // 3d4e5f6a7b3d4e5f6a7bd4e5f6a7b8c9

        // 设置token用户信息和过期时间
        tokenInfoBO.setUserInfoInToken(userInfoInToken);
        tokenInfoBO.setExpiresIn(getExpiresIn(userInfoInToken.getSysType()));

        String uidToAccessKeyStr = getUidToAccessKey(getApprovalKey(userInfoInToken)); // swiftmark_oauth:token:uid_to_access:{appId}:{uid}
        String accessKeyStr = getAccessKey(accessToken); // swiftmark_oauth:token:access:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
        String refreshToAccessKeyStr = getRefreshToAccessKey(refreshToken); // swiftmark_oauth:token:refresh_to_access:3d4e5f6a7b3d4e5f6a7bd4e5f6a7b8c9

        // 一个用户会登陆很多次，每次登陆的token都会存在 uid_to_access里面
        // 但是每次保存都会更新这个key的时间，而key里面的token有可能会过期，过期就要移除掉
        List<String> existsAccessTokens = new ArrayList<>();
        // 新的token数据 a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6:3d4e5f6a7b3d4e5f6a7bd4e5f6a7b8c9
        existsAccessTokens.add(accessToken + StrUtil.COLON + refreshToken);

        // 获取旧token集合
        Long size = redisTemplate.opsForSet().size(uidToAccessKeyStr);
        if (size != null && size != 0) {
            List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidToAccessKeyStr, size); // 弹出所有旧令牌
            if (tokenInfoBoList != null) {
                for (String accessTokenWithRefreshToken : tokenInfoBoList) {
                    String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
                    String accessTokenData = accessTokenWithRefreshTokenArr[0];
                    if (BooleanUtil.isTrue(stringRedisTemplate.hasKey(getAccessKey(accessTokenData)))) { // 检查旧访问令牌是否未过期
                        existsAccessTokens.add(accessTokenWithRefreshToken);// 保留未过期的旧令牌
                    }
                }
            }
        }

        // 批量存储数据到 Redis（管道操作）
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {

            long expiresIn = tokenInfoBO.getExpiresIn(); // 获取过期时间

            byte[] uidKey = uidToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
            byte[] refreshKey = refreshToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
            byte[] accessKey = accessKeyStr.getBytes(StandardCharsets.UTF_8);

            // 1. 将所有有效令牌（新+旧）存入用户关联的集合
            for (String existsAccessToken : existsAccessTokens) {
                connection.sAdd(uidKey, existsAccessToken.getBytes(StandardCharsets.UTF_8));
            }

            // 2. 设置用户关联集合的过期时间
            // 通过uid + sysType 保存access_token，当需要禁用用户的时候，可以根据uid + sysType 禁用用户
            connection.expire(uidKey, expiresIn);

            // 3. 刷新令牌关联访问令牌（通过 refreshToken 可获取对应的 accessToken）
            // 通过refresh_token获取用户的access_token从而刷新token
            connection.setEx(refreshKey, expiresIn, accessToken.getBytes(StandardCharsets.UTF_8));


            // 4. 访问令牌关联用户信息（通过 accessToken 可获取用户数据）
            // 通过access_token保存用户的租户id，用户id，uid
            connection.setEx(accessKey, expiresIn, Objects.requireNonNull(redisSerializer.serialize(userInfoInToken)));

            return null;
        });

        // 返回给前端是加密的token
        tokenInfoBO.setAccessToken(encryptToken(accessToken,userInfoInToken.getSysType()));
        tokenInfoBO.setRefreshToken(encryptToken(refreshToken,userInfoInToken.getSysType()));

        return tokenInfoBO;
    }

    /**
     *
     * 示例说明
     * 假设用户 user123（uid=123，sysType=APP）登录系统，流程如下：
     * 生成令牌：
     * 生成 accessToken=abc123，refreshToken=def456，设置过期时间 expiresIn=604800 秒（7 天）。
     * 清理旧令牌：
     * 检查 Redis 中 uid:123:access_tokens 集合，假设之前有旧令牌 oldAccessToken=xyz789:oldRefreshToken=uvw012。
     * 检查 access:token:xyz789 是否存在：若存在（未过期），则保留该旧令牌；若不存在（已过期），则丢弃。
     * 存储数据：
     * uid:123:access_tokens 集合保存 abc123:def456（新）和 xyz789:uvw012（旧，未过期）。
     * refresh:token:def456 存储 abc123（通过刷新令牌 def456 可获取访问令牌 abc123）。
     * access:token:abc123 存储用户信息（如 user123 的租户 ID、角色等）。
     * 返回加密令牌：
     * 对 abc123 和 def456 加密（如转为 a1b2c3 和 d4e5f6），返回给前端。
     * 总结：
     * 这段代码的核心价值是统一管理用户的多端登录令牌，通过 Redis 存储关联关系，自动清理过期令牌，
     * 确保系统中只保留有效的令牌记录，同时通过加密提升令牌传输的安全性。其典型应用场景是用户登录后，
     * 支持多设备同时在线（如手机、电脑同时登录），直到所有令牌过期才会强制下线。
     */


    /**
     * 根据系统类型获取过期时间
     * @param sysType
     * @return
     */
    private int getExpiresIn(int sysType) {
        // 3600秒
        int expiresIn = 3600;

        // 普通用户token过期时间 1小时
        if (Objects.equals(sysType, SysTypeEnum.ORDINARY.value())) {
            expiresIn = expiresIn * 24 * 30;
        }
        // 系统管理员的token过期时间 2小时
        if (Objects.equals(sysType, SysTypeEnum.MULTISHOP.value()) || Objects.equals(sysType, SysTypeEnum.PLATFORM.value())) {
            expiresIn = expiresIn * 24 * 30;
        }
        return expiresIn;
    }

    /**
     * 根据accessToken 获取用户信息
     * @param accessToken accessToken
     * @param needDecrypt 是否需要解密【不需要解码说明他本来就是accessToken，而非密文】
     * @return 用户信息
     */
    public ServerResponseEntity<UserInfoInTokenBO> getUserInfoByAccessToken(String accessToken, boolean needDecrypt) {
        if (StrUtil.isBlank(accessToken)) {
            return ServerResponseEntity.showFailMsg("accessToken is blank");
        }
        String realAccessToken;
        // 是否需要解码
        if (needDecrypt) {
            // 加密token密文来获取真正的accessToken
            ServerResponseEntity<String> decryptTokenEntity = decryptToken(accessToken);
            // 获取成功提取Data，失败传出
            if (!decryptTokenEntity.isSuccess()) {
                return ServerResponseEntity.transform(decryptTokenEntity);
            }
            realAccessToken = decryptTokenEntity.getData();
        }
        else {
            realAccessToken = accessToken;
        }
        // 根据accessToken查询redis获取到用户的信息
        UserInfoInTokenBO userInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue()
                .get(getAccessKey(realAccessToken));

        if (userInfoInTokenBO == null) {
            return ServerResponseEntity.showFailMsg("accessToken 已过期");
        }
        return ServerResponseEntity.success(userInfoInTokenBO);
    }

    /**
     * 刷新token，并返回新的token【删除原来的redis中的accessToken和refreshToken信息重新生成】
     *
     * 当accessToken过期时候，主动调用该方法，刷新accessToken就不用用户再次登录了，只有当refreshToken过期了才重新登录
     * @param refreshToken 加密后的refreshToken
     * @return
     */
    public ServerResponseEntity<TokenInfoBO> refreshToken(String refreshToken) {
        if (StrUtil.isBlank(refreshToken)) {
            return ServerResponseEntity.showFailMsg("refreshToken is blank");
        }
        // 1. 解密refreshToken获取真实的refreshToken
        ServerResponseEntity<String> decryptTokenEntity = decryptToken(refreshToken);
        if (!decryptTokenEntity.isSuccess()) {
            return ServerResponseEntity.transform(decryptTokenEntity);
        }
        String realRefreshToken = decryptTokenEntity.getData();

        // 2. 根据refreshToken获取到accessToken
        String accessToken = stringRedisTemplate.opsForValue().get(getRefreshToAccessKey(realRefreshToken));

        if (StrUtil.isBlank(accessToken)) {
            return ServerResponseEntity.showFailMsg("refreshToken 已过期");
        }
        // 3. 根据accessToken获取用户信息
        ServerResponseEntity<UserInfoInTokenBO> userInfoByAccessTokenEntity = getUserInfoByAccessToken(accessToken,
                false);

        if (!userInfoByAccessTokenEntity.isSuccess()) {
            return ServerResponseEntity.showFailMsg("refreshToken 已过期");
        }

        UserInfoInTokenBO userInfoInTokenBO = userInfoByAccessTokenEntity.getData();
        // 删除旧的refresh_token
        stringRedisTemplate.delete(getRefreshToAccessKey(realRefreshToken));
        // 删除旧的access_token
        stringRedisTemplate.delete(getAccessKey(accessToken));
        // 保存一份新的token
        TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInTokenBO);

        return ServerResponseEntity.success(tokenInfoBO);
    }

    /**
     * 删除全部的token
     */
    public void deleteAllToken(String appId, Long uid) {
        String uidKey = getUidToAccessKey(getApprovalKey(appId, uid));
        Long size = redisTemplate.opsForSet().size(uidKey);
        if (size == null || size == 0) {
            return;
        }
        List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidKey, size);

        if (CollUtil.isEmpty(tokenInfoBoList)) {
            return;
        }

        for (String accessTokenWithRefreshToken : tokenInfoBoList) {
            String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
            String accessToken = accessTokenWithRefreshTokenArr[0];
            String refreshToken = accessTokenWithRefreshTokenArr[1];
            redisTemplate.delete(getRefreshToAccessKey(refreshToken));
            redisTemplate.delete(getAccessKey(accessToken));
        }
        redisTemplate.delete(uidKey);

    }


    /**
     * 加密生成token 【accessToken【32】 + 时间戳【13】 + 系统类型【19】】
     * @param accessToken
     * @param sysType
     * @return 64位密文token
     */
    private String encryptToken(String accessToken,Integer sysType) {
        return Base64.encode(accessToken + System.currentTimeMillis() + sysType);
    }

    /**
     * 解码token密文，返回真正的accessToken
     * @param data accessToken 64位密文
     * @return 返回accessToken
     */
    private ServerResponseEntity<String> decryptToken(String data) {
        String decryptStr;
        String decryptToken;
        try {
            // 使用Base64解码传入的data字符串，将解码后的结果存储在decryptStr中。
            decryptStr = Base64.decodeStr(data);
            decryptToken = decryptStr.substring(0,32); // 对应accessToken
            // 创建token的时间，token使用时效性，防止攻击者通过一堆的尝试找到aes的密码，虽然aes是目前几乎最好的加密算法
            // 从解码后的字符串中提取第 33 到第 45 个字符，并将其解析为long类型的时间戳，表示 token 的创建时间
            long createTokenTime = Long.parseLong(decryptStr.substring(32,45));
            // 从解码后的字符串中提取第 46 个字符及之后的部分，并将其解析为int类型，表示系统类型。
            int sysType = Integer.parseInt(decryptStr.substring(45));
            // token的过期时间
            int expiresIn = getExpiresIn(sysType);
            long second = 1000L;
            // 创建时间的差值是否大于过期时间
            if (System.currentTimeMillis() - createTokenTime > expiresIn * second) {
                return ServerResponseEntity.showFailMsg("token 格式有误");
            }
        }
        catch (Exception e) {
            logger.error(e.getMessage());
            return ServerResponseEntity.showFailMsg("token 格式有误");
        }

        // 防止解密后的token是脚本，从而对redis进行攻击，uuid只能是数字和小写字母
        if (!PrincipalUtil.isSimpleChar(decryptToken)) {
            return ServerResponseEntity.showFailMsg("token 格式有误");
        }
        return ServerResponseEntity.success(decryptToken);
    }

    /**
     * 返回appId:uid
     *
     * @param userInfoInToken
     * @return appId:uid
     */
    private static String getApprovalKey(UserInfoInTokenBO userInfoInToken) {
        return getApprovalKey(userInfoInToken.getSysType().toString(), userInfoInToken.getUid());
    }

    /**
     *
     * 判断是否有appId
     * @param appId 系统类型
     * @param uid 用户id
     * @return appId:uid
     */
    private static String getApprovalKey(String appId, Long uid) {
        return uid == null?  appId : appId + StrUtil.COLON + uid;
    }

    /**
     * 为了安全加上一个前缀：swiftmark_oauth:token:access和accessToken组成redis的key
     * @param accessToken
     * @return swiftmark_oauth:token:access:
     */
    public String getAccessKey(String accessToken) {
        return CacheNames.ACCESS + accessToken;
    }

    /**
     * 为了安全加上一个前缀：swiftmark_oauth:token:uid_to_access和appId:uid组成redis的key
     * @param approvalKey
     * @return "swiftmark_oauth:token:uid_to_access:appId:uid"
     */
    public String getUidToAccessKey(String approvalKey) {
        return CacheNames.UID_TO_ACCESS + approvalKey;
    }

    /**
     * 为了安全加上一个前缀：swiftmark_oauth:token:refresh_to_access和refreshToken组成redis的key
     * @param refreshToken
     * @return
     */
    public String getRefreshToAccessKey(String refreshToken) {
        return CacheNames.REFRESH_TO_ACCESS + refreshToken;
    }

    /**
     * TokenInfoBO转TokenInfoVO，VO用于前端展示
     * @param userInfoInToken
     * @return
     */
    public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInToken) {
        TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInToken);

        TokenInfoVO tokenInfoVO = new TokenInfoVO();
        tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken());
        tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken());
        tokenInfoVO.setExpiresIn(tokenInfoBO.getExpiresIn());
        return tokenInfoVO;
    }

    /**
     * 修改accessToken中对应的用户信息
     *
     * @param uid
     * @param appId
     * @param userInfoInTokenBO
     */
    public void updateUserInfoByUidAndAppId(Long uid, String appId, UserInfoInTokenBO userInfoInTokenBO) {
        if (userInfoInTokenBO == null) {
            return;
        }
        // 1. 获取用户的uidkey【redis中用户Token集合的key】
        String uidKey = getUidToAccessKey(getApprovalKey(appId, uid));
        // 2. 找到该用户的Token列表
        Set<String> tokenInfoBoList = stringRedisTemplate.opsForSet().members(uidKey);
        if (tokenInfoBoList == null || tokenInfoBoList.size() == 0) {
            throw new SwiftMarkException(ResponseEnum.UNAUTHORIZED);
        }
        // 3. 遍历列表，获取到用户信息更新用户信息
        for (String accessTokenWithRefreshToken : tokenInfoBoList) {
            String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
            // 获取accessToken
            String accessKey = this.getAccessKey(accessTokenWithRefreshTokenArr[0]);
            // 根据accessToken获取到用户信息
            UserInfoInTokenBO oldUserInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue().get(accessKey);
            if (oldUserInfoInTokenBO == null) {
                continue;
            }
            // 更新用户信息，并重新设置过期时间
            BeanUtils.copyProperties(userInfoInTokenBO, oldUserInfoInTokenBO);
            redisTemplate.opsForValue().set(accessKey, Objects.requireNonNull(userInfoInTokenBO),getExpiresIn(userInfoInTokenBO.getSysType()), TimeUnit.SECONDS);
        }
    }
}
